package containerizedengine

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/containerd/containerd"
	"github.com/containerd/containerd/content"
	"github.com/containerd/containerd/errdefs"
	"github.com/containerd/containerd/images"
	"github.com/containerd/containerd/namespaces"
	"github.com/docker/distribution/reference"
	ver "github.com/hashicorp/go-version"
	v1 "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/pkg/errors"
	"strings"
	"xemo.top/docker/cli/cli/config/types"
	"xemo.top/docker/cli/internal/versions"
	clitypes "xemo.top/docker/cli/types"
)

// ActivateEngine 会将映像从CE切换到EE映像
// ActivateEngine will switch the image from the CE to EE image
func (c *baseClient) ActivateEngine(ctx context.Context, opts clitypes.EngineInitOptions, out clitypes.OutStream, authConfig *types.AuthConfig) error {
	// 如果用户未指定映像，请确定要使用的正确企业映像
	// If the user didn't specify an image,determine the correct enterprise image to use
	if opts.EngineImage == "" {
		localMetadata, err := versions.GetCurrentRuntimeMetadata(opts.RuntimeMetadataDir)
		if err != nil {
			//return errors.Wrap(err,"unable to determine the installed engine version.Specify which engin image to update with --engin-image")
			return errors.Wrap(err, "无法确定已安装的引擎版本。请指定要更新的引擎映像 --engin-image")
		}

		engineImage := localMetadata.EngineImage
		if engineImage == clitypes.EnterpriseEngineImage || engineImage == clitypes.CommunityEngineImage {
			opts.EngineImage = clitypes.EnterpriseEngineImage
		} else {
			// 切掉标准前缀并保留任何尾随操作系统特定的映像详细信息
			// Chop off the standard prefix and retain any trailing OS specific image details
			// e.g. engine-community-dm -> engine-enterprise-dm
			engineImage = strings.TrimPrefix(engineImage, clitypes.EnterpriseEngineImage)
			engineImage = strings.TrimPrefix(engineImage, clitypes.CommunityEngineImage)
			opts.EngineImage = clitypes.EnterpriseEngineImage + engineImage
		}
	}

	ctx = namespaces.WithNamespace(ctx, engineNamespace)
	return c.DoUpdate(ctx, opts, out, authConfig)
}

// DoUpdate 执行基础引擎更新
// DoUpdate performs the underlying engine update
func (c *baseClient) DoUpdate(ctx context.Context, opts clitypes.EngineInitOptions, out clitypes.OutStream, authConfig *types.AuthConfig) error {
	ctx = namespaces.WithNamespace(ctx, engineNamespace)
	if opts.EngineVersion == "" {
		// TODO - Future enhancement: This could be improved to be smart about figuring out the latest patch rev for the current engine version and automatically apply it so users could stay in sync by simply having a scheduled `docker engine update`
		// TODO - 未来的增强: 这可以改进为聪明地找出当前引擎版本的最新补丁版本，并自动应用它，这样用户就可以通过简单地安排一个 “docker engine update” 来保持同步
		//return fmt.Errorf("pick the version you want to update to with --version")
		return fmt.Errorf("选择要更新到的版本 --version")
	}
	var localMetadata *clitypes.RuntimeMetadata
	if opts.EngineImage == "" {
		var err error
		localMetadata, err = versions.GetCurrentRuntimeMetadata(opts.RuntimeMetadataDir)
		if err != nil {
			//return errors.Wrap(err,"unable to determine the installed engine version.Specify which engine image to update with --engine-image set to 'engine-community' or 'engine-enterprise'")
			return errors.Wrap(err, "无法确定已安装的引擎版本。请指定要更新的引擎映像 --engine-image set to 'engine-community' or 'engine-enterprise'")
		}
		opts.EngineImage = localMetadata.EngineImage
	}
	imageName := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion)

	// Look for desired image
	// 查找所需的image
	image, err := c.cclient.GetImage(ctx, imageName)
	if err != nil {
		if errdefs.IsNotFound(err) {
			image, err = c.pullWithAuth(ctx, imageName, out, authConfig)
			if err != nil {
				return errors.Wrapf(err, "unable to pull image %s", imageName)
			}
		} else {
			return errors.Wrapf(err, "unable to check for image %s", imageName)
		}
	}

	// 确保我们可以安全进行
	// Make sure we're safe to proceed
	newMetadata, err := c.PreflightCheck(ctx, image)
	if err != nil {
		return err
	}
	if localMetadata != nil {
		if localMetadata.Platform != newMetadata.Platform {
			fmt.Fprintf(out, "\nNotice: you have switched to \"%s\".Refer to %s for update instructions.\n\n", newMetadata.Platform, getReleaseNotesURL(imageName))
		}
	}

	if err := c.cclient.Install(ctx, image, containerd.WithInstallReplace, containerd.WithInstallPath("/usr")); err != nil {
		return err
	}
	return versions.WriteRuntimeMetadata(opts.RuntimeMetadataDir, newMetadata)
}

// PreflightCheck 在继续更新/激活之前，验证指定的映像是否与本地系统兼容
// PreflightCheck verifies the specified image is compatible with the local system before proceeding to update/activate
// 如果看起来不错，则会返回新映像的RuntimeMetadata，并可以将其写出到主机
// If things look good, the RuntimeMetadata for the new image is returned and can be written out to the host
func (c *baseClient) PreflightCheck(ctx context.Context, image containerd.Image) (*clitypes.RuntimeMetadata, error) {
	var metadata clitypes.RuntimeMetadata
	ic, err := image.Config(ctx)
	if err != nil {
		return nil, err
	}
	var (
		ociimage v1.Image
		config   v1.ImageConfig
	)
	switch ic.MediaType {
	case v1.MediaTypeImageConfig, images.MediaTypeDockerSchema2Config:
		p, err := content.ReadBlob(ctx, image.ContentStore(), ic)
		if err != nil {
			return nil, err
		}
		if err := json.Unmarshal(p, &ociimage); err != nil {
			return nil, err
		}
		config = ociimage.Config
	default:
		return nil, fmt.Errorf("unknown image %s config media type %s", image.Name(), ic.MediaType)
	}

	metadataString, ok := config.Labels["com.docker."+clitypes.RuntimeMetadataName]
	if !ok {
		return nil, fmt.Errorf("image %s does not contain runtime metadata label %s", image.Name(), clitypes.RuntimeMetadataName)
	}
	err = json.Unmarshal([]byte(metadataString), &metadata)
	if err != nil {
		return nil, errors.Wrapf(err, "malformed runtime metadata file in %s", image.Name())
	}

	// Current CLI 仅支持主机安装运行时
	// Current CLI only supports host install runtime
	if metadata.Runtime != "host_install" {
		return nil, fmt.Errorf("unsupported daemon image: %s\nConsult the release notes at %s for upgrade instructions", metadata.Runtime, getReleaseNotesURL(image.Name()))
	}

	// Verify local containerd is new enough
	localVersion, err := c.cclient.Version(ctx)
	if err != nil {
		return nil, err
	}
	if metadata.ContainerdMinVersion != "" {
		lv, err := ver.NewVersion(localVersion.Version)
		if err != nil {
			return nil, err
		}
		mv, err := ver.NewVersion(metadata.ContainerdMinVersion)
		if err != nil {
			return nil, err
		}
		if lv.LessThan(mv) {
			return nil, fmt.Errorf("local containerd is too old: %s - this engine vcersion requires %s or newer.\nConsult the release notes at %s for upgrade instructions", localVersion.Version, metadata.ContainerdMinVersion, getReleaseNotesURL(image.Name()))
		}
	}
	// 如果在元数据上省略，则在18.09基线之外对containerd版本没有硬依赖
	// If omitted on metadata,no hard dependency on containerd version beyond 18.09 baseline
	// 所有检查看起来好，继续更新
	// All checks look OK,proceed with update
	return &metadata, nil
}

// getReleaseNotesURL 返回发行说明url
// getReleaseNotesURL returns a release notes url
// 如果图像名称不包含版本标记，则返回基本发行说明URL
// If the image name does not contain a version tag,the base release notes URL is returned
func getReleaseNotesURL(imageName string) string {
	versionTag := ""
	distributionRef, err := reference.ParseNormalizedNamed(imageName)
	if err == nil {
		taggedRef, ok := distributionRef.(reference.NamedTagged)
		if ok {
			versionTag = taggedRef.Tag()
		}
	}
	return fmt.Sprintf("%s/%s", clitypes.ReleaseNotePrefix, versionTag)
}
